/*
 * Copyright (c) 2011 Norbert Kopcsek
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.modulr.feature {

	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.HTTPStatusEvent;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.utils.Dictionary;

	import mx.collections.ArrayList;
	import mx.collections.IList;

	import org.as3commons.logging.ILogger;
	import org.as3commons.logging.LoggerFactory;
	import org.modulr.bundle.Bundle;
	import org.modulr.bundle.RequiredFeature;
	import org.modulr.framework.Framework;

	[Event( name="complete", type="flash.events.Event" )]
	/**
	 * Manages all features loaded by the framework.
	 *
	 * @see org.modulr.feature.Feature
	 * @see org.modulr.framework.Framework
	 *
	 * @author Norbert Kopcsek
	 */
	public class FeatureRegistry extends EventDispatcher {


		//------------------------------------------------------------------------------
		//
		//   Statics 
		//
		//------------------------------------------------------------------------------

		private static var logger:ILogger = LoggerFactory.getLogger( "FeatureRegistry" );


		//------------------------------------------------------------------------------
		//
		//   Constructor 
		//
		//------------------------------------------------------------------------------

		public function FeatureRegistry( framework:Framework ) {
			this.framework = framework;

			_features = new ArrayList();
			featuresById = new Dictionary();
		}


		//------------------------------------------------------------------------------
		//
		//   Properties (Getter/Setter) 
		//
		//------------------------------------------------------------------------------

		//--------------------------------------
		// features 
		//--------------------------------------

		public function get features():IList {
			return _features;
		}


		//------------------------------------------------------------------------------
		//
		//   Protected fields 
		//
		//------------------------------------------------------------------------------

		/**
		 * @private
		 */
		protected var _features:IList;

		/**
		 * @private
		 */
		protected var featuresById:Dictionary;

		/**
		 * @private
		 */
		protected var framework:Framework;


		//------------------------------------------------------------------------------
		//
		//  Methods 
		//
		//------------------------------------------------------------------------------

		public function loadFeaturesXml( urlRequest:URLRequest ):void {
			var featuresXmlLoader:URLLoader = new URLLoader();
			featuresXmlLoader.dataFormat = URLLoaderDataFormat.TEXT;
			featuresXmlLoader.addEventListener( Event.COMPLETE, featuresXmlLoader_completeHandler ); // no weak reference
			featuresXmlLoader.load( urlRequest );
		}

		/**
		 * Loads all mandatory features.
		 */
		public function loadMandatoryFeatures():void {
			for ( var i:int = 0; i < features.length; i++ ) {
				var feature:Feature = features.getItemAt( i ) as Feature;

				if ( feature.state == FeatureState.INSTALLED && feature.mandatory ) {
					loadFeature( feature );
				}
			}
		}


		public function loadFeature( feature:Feature ):void {
			if ( feature == null ) { // this or error
				return;
			}

			loadOrQueueFeature( feature );
		}

		public function getFeatureById( id:String ):Feature {
			if ( containsFeature( id )) {
				return featuresById[ id ];
			} else {
				return null;
			}
		}

		public function containsFeature( id:String ):Boolean {
			return featuresById.hasOwnProperty( id );
		}

		/**
		 * Parses feature.xml and creates feature descriptors.
		 * @param xml The xml object to be parsed.
		 */
		public function installFeaturesFromXml( xml:XML ):void {
			for each ( var featureXml:XML in xml.feature ) {
				if ( !containsFeature( featureXml.@id.toString())) {
					installFeature( createFeatureFromXml( featureXml, xml ));
				}
			}
		}

		public function installFeature( feature:Feature ):Feature {
			if ( containsFeature( feature.featureId )) {
				return getFeatureById( feature.featureId );
			}

			for ( var i:int = 0; i < feature.bundles.length; i++ ) {
				var bundle:Bundle = feature.bundles[ i ] as Bundle;
				framework.bundleRegistry.installBundle( bundle );
			}

			features.addItem( feature );
			featuresById[ feature.featureId ] = feature;
			return feature;
		}

		public function createFeatureFromXml( featureXml:XML, xml:XML ):Feature {
			if ( containsFeature( featureXml.@id.toString())) {
				return getFeatureById( featureXml.@id.toString());
			}

			var feature:Feature = new Feature( featureXml.@id.toString(), featureXml.@name.toString(), featureXml.@url.toString(), featureXml.@mandatory == "true" );

			var requiredFeatureXmlList:XMLList = featureXml.requiredFeature;

			for ( var i:int = 0; i < requiredFeatureXmlList.length(); i++ ) {
				var requiredFeatureXml:XML = requiredFeatureXmlList[ i ];
				var requiredFeatureId:String = requiredFeatureXml.@id.toString();

				var requiredFeature:RequiredFeature = new RequiredFeature( requiredFeatureId );
				feature.requiredFeatures.push( requiredFeature );
			}

			var bundlesXmlList:XMLList = featureXml.bundles;

			for ( var j:int = 0; j < bundlesXmlList.length(); j++ ) {
				var bundleXmlList:XML = bundlesXmlList[ j ];
				feature.bundles.push( framework.bundleRegistry.createBundleFromXml( bundleXmlList ));
			}

			return feature;
		}


		//------------------------------------------------------------------------------
		//
		//   Protected methods 
		//
		//------------------------------------------------------------------------------

		/**
		 * @private
		 */
		protected function loadOrQueueFeature( feature:Feature ):void {
			if ( feature.state != FeatureState.INSTALLED && feature.state != FeatureState.QUEUED ) {
				return;
			}

			installDependencies( feature );

			if ( areRequiredFeaturesLoaded( feature )) {

				logger.info( "Loading feature {0}", feature.featureId );

				loadDependencies( feature );

				var featureLoader:FeatureLoader = new FeatureLoader( feature );
				addEventListenersToFeatureLoader( featureLoader );
				featureLoader.load();

				feature.state = FeatureState.LOADING;
					//dispatchEvent( new BundleEvent( BundleEvent.LOADING, feature ));
			} else if ( feature.state !== FeatureState.QUEUED ) {
				logger.info( "Queuing feature {0}", feature.featureId );

				feature.state = FeatureState.QUEUED;

				for ( var i:uint = 0; i < feature.requiredFeatures.length; i++ ) {
					var requiredFeature:RequiredFeature = feature.requiredFeatures[ i ];
					loadOrQueueFeature( featuresById[ requiredFeature.featureId ] as Feature );
				}
			}

		}

		/**
		 * @private
		 */
		protected function tryLoadQueuedFeatures( features:Array ):void {
			for ( var i:int = 0; i < features.length; i++ ) {
				var feature:Feature = features[ i ] as Feature;

				if ( feature.state == FeatureState.QUEUED && areRequiredFeaturesLoaded( feature )) {
					loadOrQueueFeature( feature ); // TODO better would be "load"
				}
			}
		}

		/**
		 * Checks whether all required features of the given feature are loaded or not.
		 * @private
		 * @param feature The feature to be checked.
		 * @return Returns only true if all required features are active.
		 */
		protected function areRequiredFeaturesLoaded( feature:Feature ):Boolean {
			for ( var i:uint = 0; i < feature.requiredFeatures.length; i++ ) {
				var requiredFeature:RequiredFeature = feature.requiredFeatures[ i ] as RequiredFeature;

				if (( featuresById[ requiredFeature.featureId ] as Feature ).state != FeatureState.LOADED ) {
					return false;
				}
			}
			return true;
		}

		/**
		 * Installs the dependencies of the given feature.
		 * @private
		 * @param feature The feature for which the dependencies should be installed.
		 */
		protected function installDependencies( feature:Feature ):void {
			for ( var i:uint = 0; i < feature.requiredFeatures.length; i++ ) {
				var requiredFeature:RequiredFeature = feature.requiredFeatures[ i ] as RequiredFeature;

				if ( requiredFeature.feature == null ) {
					requiredFeature.feature = featuresById[ requiredFeature.featureId ];
					requiredFeature.feature.requiredByFeatures.push( feature );
				}
			}
		}

		/**
		 * @private
		 */
		protected function loadDependencies( feature:Feature ):void {
			for ( var i:uint = 0; i < feature.requiredFeatures.length; i++ ) {
				var requiredFeature:RequiredFeature = feature.requiredFeatures[ i ] as RequiredFeature;
				var actualRequiredFeature:Feature = featuresById[ requiredFeature.featureId ] as Feature;
				loadOrQueueFeature( actualRequiredFeature );
			}
		}

		/**
		 * @private
		 */
		protected function addEventListenersToFeatureLoader( featureLoader:FeatureLoader ):void {
			featureLoader.addEventListener( Event.COMPLETE, featureLoader_completeHandler ); // no weak reference
			featureLoader.addEventListener( ErrorEvent.ERROR, featureLoader_errorHandler ); // no weak reference
		}

		/**
		 * @private
		 */
		protected function removeEventListenersFromFeatureLoader( featureLoader:FeatureLoader ):void {
			featureLoader.removeEventListener( Event.COMPLETE, featureLoader_completeHandler );
			featureLoader.removeEventListener( ErrorEvent.ERROR, featureLoader_errorHandler );
		}

		/**
		 * @private
		 * Called when features.xml is loaded.
		 */
		protected function featuresXmlLoader_completeHandler( event:Event ):void {
			var featuresXmlLoader:URLLoader = event.target as URLLoader;
			featuresXmlLoader.removeEventListener( Event.COMPLETE, featuresXmlLoader_completeHandler );
			// TODO Need to check if this is proper xml
			// TODO Test this stuff
			installFeaturesFromXml( new XML( event.target.data ));

			dispatchEvent( new Event( Event.COMPLETE ));
			loadMandatoryFeatures();
		}

		/**
		 * @private
		 */
		protected function featureLoader_completeHandler( event:Event ):void {
			var featureLoader:FeatureLoader = event.target as FeatureLoader;
			removeEventListenersFromFeatureLoader( featureLoader );

			var feature:Feature = featureLoader.feature;
			logger.info( "Loaded feature {0}", feature.featureId );
			feature.state = FeatureState.LOADED;

			tryLoadQueuedFeatures( feature.requiredByFeatures );
			dispatchEvent( new FeatureEvent( FeatureEvent.FEATURE_LOADED, feature ));
		}

		/**
		 * @private
		 */
		protected function featureLoader_errorHandler( event:ErrorEvent ):void {
			var featureLoader:FeatureLoader = event.target as FeatureLoader;
			removeEventListenersFromFeatureLoader( featureLoader );

			var feature:Feature = featureLoader.feature;
			logger.error( "Could not load feature {0}", feature.featureId );
			feature.state = FeatureState.ERROR;
		}
	}
}

